<?php
/**
 * Zend_Jabber is a main jabber client class
 *
 * This class manages user connection and address book. You can send requests through this class
 * by registering a callback function witch will be executed when response arrives. You can also
 * wait for particular response to arrive
 */
class Zend_Jabber {
    /**
     * Logged jabber user or null if user is not authorized or connection is not opened
     * 
     * @var Zend_Jabber_User
     */
    private $loggedUser;
       
    /**
     * Connection wrapper
     *
     * @var Zend_Jabber_Connection
     */
    private $connection;
    
    /**
     * Registered callbacks
     * 
     * @var array
     */
    private $registeredCallbacks = array();
    
    /**
     * This variable is used when receiving response. 
     *
     * When it is more than zero script wont wait for a response more than specified time. This
     * variable is used to calculate how mutch time it pased since the countdown started
     * 
     * @var integer
     */
    private static $startTime = false;
    
    /**
     * Address book instance
     * 
     * @var Zend_Jabber_AddressBook
     */
    private $addressBook;
    
    /**
     * Create new Zend_Jabber object
     *
     * To connect to the server use connect method
     */ 
    public function __construct() {
        $this->registerCallback('IQ.Roster.Set', array($this, 'pushRoster'));
        $this->registerCallback('Presence', array($this, 'gotPresence'));
    }
    
    /**
     * Connect to Jabber server
     *
     * @param string $host Zend_Jabber server address
     * @param string $port Zend_Jabber server port
     * @param string $domain Zend_Jabber server domain name
     */
    public function connect($host, $port = 5222, $domain = false) {
        if(!$domain) $domain = $host;
        $this->connection = new Zend_Jabber_Connection($host, $port, $domain);
        $this->connection->open();
    }
    
    /**
     * Login with existing username and password
     *
     * @param string $username Your Zend_Jabber username
     * @param string $password Your Zend_Jabber password
     * @return Zend_Jabber_Stanza_Response_Auth
     */
    public function login(Zend_Jabber_User $user, $password) {
        if($this->isLoggedIn()) throw new Zend_Jabber_Exception(Zend_Jabber_Exception::MESSAGE_LOGGED_IN, Zend_Jabber_Exception::CODE_LOGGED_IN);
        
        $request = new Zend_Jabber_Stanza_Request_Auth($user, $password);    
        $this->connection->send($request);
        $response = $this->receive($request);
        
        if($response->getType() == 'success') {
	        $this->loggedUser = $user;

            $this->connection->createStreams();
	        
            // Now we should do everything the server sais to complete the authentication process
	        $features = $this->connection->getFeatures();
	        if(isset($features['bind'])) $this->bind($user);
	        if(isset($features['session'])) $this->session();
	        
	        // According to RFC3921 part 7.4. it is good practice to receive a roster immediately after login
	        // and just before initial presence
	        $request = new Zend_Jabber_Stanza_Request_Iq_Roster_Get();
	        $this->connection->send($request);
	        $this->addressBook = $this->receive($request)->getAddressBook();
	        
	        // Send initial presence just after roster retrival
	        $request = new Zend_Jabber_Stanza_Request_Presence_Normal();
	        $this->connection->send($request);
        }
        
        return $response;
    }
    
    /**
     * Get user address book local copy
     * 
     * @return Zend_Jabber_AddressBook
     */
    public function getAddressBook() {
        return $this->addressBook;
    }

    /**
     * Send text message to another user
     *
     * @param Zend_Jabber_User $username Destination user
     * @param string $message Message body
     * @return Zend_Jabber_Stanza_Response_Message_Chat
     */
    public function message(Zend_Jabber_User $user, $message) {
        if(!$this->isLoggedIn()) throw new Zend_Jabber_Exception(Zend_Jabber_Exception::MESSAGE_NOT_LOGGED_IN, Zend_Jabber_Exception::CODE_NOT_LOGGED_IN);
        
        $request =new  Zend_Jabber_Stanza_Request_Message_Chat($user, $message, $includeResource);
        $this->connection->send($request);
    }
    
    /**
     * Register new Zend_Jabber user
     *
     * @param Zend_Jabber_User $user User
     * @param string $password Password
     * @return Zend_Jabber_Stanza_Response_Iq_Register_Set
     */
    public function register(Zend_Jabber_User $user, $password) {
        if($this->isLoggedIn()) throw new Zend_Jabber_Exception(Zend_Jabber_Exception::MESSAGE_LOGGED_IN, Zend_Jabber_Exception::CODE_LOGGED_IN);
        
        $request = new Zend_Jabber_Stanza_Request_Iq_Register_Set($user, $password);
        $this->connection->send($request);
        
        return $this->receive($request);
    }
    
    /**
     * Get loged user or false if user is not authenticated
     * 
     * @return Zend_Jabber_User
     */
    public function getLoggedUser() {
        return $this->loggedUser;
    }
    
    /**
     * Return true if user is successfully authenticated and false if not
     * 
     * @return boolean
     */
    public function isLoggedIn() {
        return $this->loggedUser ? true : false;
    }
    
    /**
     * Synchronize presences with server
     *
     * Update your presence copy on the server and retreve other users presences
     */
    public function updatePresence() {
        if(!$this->isLoggedIn()) throw new Zend_Jabber_Exception(Zend_Jabber_Exception::MESSAGE_NOT_LOGGED_IN, Zend_Jabber_Exception::CODE_NOT_LOGGED_IN);
        
        $request = new Zend_Jabber_Stanza_Request_Presence_Normal($this->getLoggedUser());
        $this->connection->send($request);
    }
    
    /**
     * Ask user to allow you to see his presence
     * 
     * @param Zend_Jabber_User $user Distant user
     */
    public function subscribe(Zend_Jabber_User $user) {
        if(!$this->isLoggedIn()) throw new Zend_Jabber_Exception(Zend_Jabber_Exception::MESSAGE_NOT_LOGGED_IN, Zend_Jabber_Exception::CODE_NOT_LOGGED_IN);
        
        $request = new Zend_Jabber_Stanza_Request_Presence_Subscribe($user);
        $this->connection->send($request); 
    }
    
    /**
     * Deny user see your presence
     * 
     * @param Zend_Jabber_User $user Distant user
     */
    public function unsubscribe(Zend_Jabber_User $user) {
        if(!$this->isLoggedIn()) throw new Zend_Jabber_Exception(Zend_Jabber_Exception::MESSAGE_NOT_LOGGED_IN, Zend_Jabber_Exception::CODE_NOT_LOGGED_IN);
        
        $request = new Zend_Jabber_Stanza_Request_Presence_Unsubscribe($user);
        $this->connection->send($request); 
    }
    
    /**
     * Disconnect from server
     */
    public function disconnect() {
        $this->connection->close();
    }
    
    /**
     * Receive response from server
     * 
     * You can also pass two parameters. If you specify first parameter this method will drop
     * all responses except until a response to a given request arrives. Second parameters is
     * used only when request is specified. Passing $time you specify a limit to how much time
     * program should wait for a response
     * 
     * @param Zend_Jabber_Stanza_Request_Abstract $request If request is specified this function will return only when a response to specified request will arrive
     * @param integer $time Wait no more than specified number of seconds (-1 to set no limit) 
     * @return Zend_Jabber_Stanza_Response_Abstract
     */
    public function receive(Zend_Jabber_Stanza_Request_Abstract $request = null, $time = -1) {
        if(!$request) {
            $response = $this->connection->receive();
            
   	        if(isset($this->registeredCallbacks[get_class($response)])) {
   	            foreach($this->registeredCallbacks[get_class($response)] as $callback) {
   	                call_user_func($callback, $response);
   	            }
	            
	            return new Zend_Jabber_Stanza_Null();
	        }
	        
	        return $response;
        } else { 
	        if(!self::$startTime && $time > 0) {
	             self::$startTime = time();
	        }
            
            // Wait for response to a particular request
	        while($response = $this->connection->receive()) {
	            if($request->getClass() == $response->getClass()) return $response;
	            if($time > 0 && (time() - self::$startTime < 2)) break;
	        }

	        if($time > 0) {
	             self::$startTime = false;
	        }
	        
	        return false;
        }
    }

    /**
     * Change user groups or add new user. By using this funtion you are updating roster information (see Zend_Jabber::getAddressBook())
     *
     * @param Zend_Jabber_User $user Distant user
     * @param array $groupNames Array of group names of wich distant user is member of  
     */
    public function setContact(Zend_Jabber_User $user, $groupNames = array()) {
        if(!$this->isLoggedIn()) throw new Zend_Jabber_Exception(Zend_Jabber_Exception::MESSAGE_NOT_LOGGED_IN, Zend_Jabber_Exception::CODE_NOT_LOGGED_IN);
        
        $groups = array();
        foreach($groupNames as $groupName) {
            if(!($group = $this->getAddressBook()->getGroupByName($groupName))) {
                $group = new Zend_Jabber_AddressBook_Group($this->addressBook, $groupName);
            }
            
            $groups[] = $group; 
        }
        
        $item = new Zend_Jabber_AddressBook_Item($this->getAddressBook(), $user, Zend_Jabber_AddressBook_Item::SUBSCRIPTION_NONE, $groups);
        
        $request = new Zend_Jabber_Stanza_Request_Iq_Roster_Set($item);
        $this->connection->send($request);
    }
    
    /**
     * Remove user from address book
     * 
     * @param Zend_Jabber_User $user Distant user
     */
    public function removeContact(Zend_Jabber_User $user) {
        if(!$this->isLoggedIn()) throw new Zend_Jabber_Exception(Zend_Jabber_Exception::MESSAGE_NOT_LOGGED_IN, Zend_Jabber_Exception::CODE_NOT_LOGGED_IN);
        
        $item = new Zend_Jabber_AddressBook_Item($this->getAddressBook(), $user, Zend_Jabber_AddressBook_Item::SUBSCRIPTION_REMOVE);
        
        $request = new Zend_Jabber_Stanza_Request_Iq_Roster_Set($item);
        $this->connection->send($request);
    }
    
    /**
     * Register a callback function.
     *
     * Callback function is called when response from server is received 
     * 
     * @param string $className Recived stanza id
     * @param mixed $callback Callback
     */
    public function registerCallback($stanza, $callback) {
        $className = 'Zend_Jabber_Stanza_Response_' . str_replace('.', '_', $stanza);
        if(!is_array($this->registeredCallbacks[$className])) {
            $this->registeredCallbacks[$className] = array();
        }
        
        $this->registeredCallbacks[$className][$this->getCallbackId($callback)] = $callback;
    }
    
    /**
     * Unregister a callback function.
     *
     * Callback function is called when response from server is received 
     * 
     * @param string $className Stanza identifying string
     * @param mixed $callback Callback
     */
    public function unregisterCallback($stanza, $callback) {
        $className = 'Zend_Jabber_Stanza_Response_' . str_replace('.', '_', $stanza);
        
        if(isset($this->registeredCallbacks[$className][$this->getCallbackId($callback)])) {
            unset($this->registeredCallbacks[$className][$this->getCallbackId($callback)]);
        }
    }

    /**
     * Update address book. 
     *
     * Update local copy of address book when roster response arrives. User can get get local
     * copy of address book through Zend_Jabber_AddressBook object
     *
     * @param Zend_Jabber_Stanza_Response_Presence $response Response object
     * @see self::getAddressBook()
     */
    private function pushRosterCallback(Zend_Jabber_Stanza_Response_Iq_Roster_Set $response) {
	     $response->createRostersItems($this->addressBook);
    }
    
    /**
     * Update contact presence. 
     *
     * This method is executed when response with user presence arrives
     * This functionality should be handled by Zend_Jabber class. User can get contact status 
     * through address book (Zend_Jabber_AddressBook)
     *
     * @param Zend_Jabber_Stanza_Response_Presence $response Response object
     * @see self::getAddressBook()
     */    
    private function gotPresenceCallback(Zend_Jabber_Stanza_Response_Presence $response) {
        foreach($this->getAddressBook()->getUsers() as $user) {
            if($user->compare($pUser = $presence->getUser())) {
                $user->setShow($pUser->getShow());
                $user->setPriority($pUser->getPriority());
                $user->setStatus($pUser->getStatus());
            } else {
                $user->setShow(Zend_Jabber_User::SHOW_OFFLINE);
            }
        }
    }
        
    /**
     * Bind resource. 
     *
     * This method gets user JID from server and changes Zend_Jabber_User parameters 
     * to those provided by server. This method should not be called by user.
     * 
     * @return Zend_Jabber_Stanza_Response_Iq_Bind_Result
     */
    private function bind() {
        if(!$this->isLoggedIn()) throw new Zend_Jabber_Exception(Zend_Jabber_Exception::MESSAGE_NOT_LOGGED_IN, Zend_Jabber_Exception::CODE_NOT_LOGGED_IN);
        
        $request = new Zend_Jabber_Stanza_Request_Iq_Bind_Set($this->loggedUser);
        $this->connection->send($request);
        
        return $this->receive($request);
    }
    
    /**
     * Start new session according to RFC3921 part 3
     * 
     * @return Zend_Jabber_Stanza_Response_Iq_Session_Result
     */
    private function session() {
        if(!$this->loggedUser) throw new Zend_Jabber_Exception(Zend_Jabber_Exception::MESSAGE_NOT_LOGGED_IN, Zend_Jabber_Exception::CODE_NOT_LOGGED_IN);
        
        $request = new Zend_Jabber_Stanza_Request_Iq_Session_Set($this->connection);
        $this->connection->send($request);
            
        return $this->receive($request);
    }
    
    /**
     * Generate uniq string identifier for each callback
     * 
     * @param string $callback Callback
     * @return string Callback unique id 
     */
    private function getCallbackId($callback) {
        if(is_array($callback)) {
            if(is_string($callback[0])) {
                $callbackId = $callback[0] . "::" . $callback[1];
            } else {
                $callbackId = spl_object_hash($callback[0]) . "->" . $callback[1];
            }
        } else {
            $callbackId = $callback;
        }

        return $callbackId;
    }
}
?>
